package com.hantanger.weblog.freemarker;

import java.io.IOException;
import java.io.Writer;
import java.util.Map;

import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
import freemarker.template.TemplateTransformModel;

/**
 * 代码从StandardCompress中拷贝过来的。 StandardCompress会把多个连续的中文全角空格也压缩成一个，此改动就是为了解决这个问题
 * 把原来判断是否为空白字符的Character.isWhitespace(c)的作了进一步限制，即c < 255 &&
 * Character.isWhitespace(c) 这样只处理英文ascii中的空白字符
 * 
 * @author lvyusheng
 * 
 */
public class CompressCN implements TemplateTransformModel {
	private static final String BUFFER_SIZE_KEY = "buffer_size";
	private static final String SINGLE_LINE_KEY = "single_line";
	private int defaultBufferSize;

	public static final CompressCN INSTANCE = new CompressCN();

	public CompressCN() {
		this(2048);
	}

	/**
	 * @param defaultBufferSize
	 *            the default amount of characters to buffer
	 */
	public CompressCN(int defaultBufferSize) {
		this.defaultBufferSize = defaultBufferSize;
	}

	@SuppressWarnings("rawtypes")
	public Writer getWriter(final Writer out, Map args)
			throws TemplateModelException {
		int bufferSize = defaultBufferSize;
		boolean singleLine = false;
		if (args != null) {
			try {
				TemplateNumberModel num = (TemplateNumberModel) args
						.get(BUFFER_SIZE_KEY);
				if (num != null)
					bufferSize = num.getAsNumber().intValue();
			} catch (ClassCastException e) {
				throw new TemplateModelException(
						"Expecting numerical argument to " + BUFFER_SIZE_KEY);
			}
			try {
				TemplateBooleanModel flag = (TemplateBooleanModel) args
						.get(SINGLE_LINE_KEY);
				if (flag != null)
					singleLine = flag.getAsBoolean();
			} catch (ClassCastException e) {
				throw new TemplateModelException(
						"Expecting boolean argument to " + SINGLE_LINE_KEY);
			}
		}
		return new StandardCompressWriter(out, bufferSize, singleLine);
	}

	private static class StandardCompressWriter extends Writer {
		private static final int MAX_EOL_LENGTH = 2; // CRLF is two bytes

		private static final int AT_BEGINNING = 0;
		private static final int SINGLE_LINE = 1;
		private static final int INIT = 2;
		private static final int SAW_CR = 3;
		private static final int LINEBREAK_CR = 4;
		private static final int LINEBREAK_CRLF = 5;
		private static final int LINEBREAK_LF = 6;

		private final Writer out;
		private final char[] buf;
		private final boolean singleLine;

		private int pos = 0;
		private boolean inWhitespace = true;
		private int lineBreakState = AT_BEGINNING;

		public StandardCompressWriter(Writer out, int bufSize,
				boolean singleLine) {
			this.out = out;
			this.singleLine = singleLine;
			buf = new char[bufSize];
		}

		public void write(char[] cbuf, int off, int len) throws IOException {
			for (;;) {
				// Need to reserve space for the EOL potentially left in the
				// state machine
				int room = buf.length - pos - MAX_EOL_LENGTH;
				if (room >= len) {
					writeHelper(cbuf, off, len);
					break;
				} else if (room <= 0) {
					flushInternal();
				} else {
					writeHelper(cbuf, off, room);
					flushInternal();
					off += room;
					len -= room;
				}
			}
		}

		private void writeHelper(char[] cbuf, int off, int len) {
			for (int i = off, end = off + len; i < end; i++) {
				char c = cbuf[i];
				if (c < 255 && Character.isWhitespace(c)) {
					inWhitespace = true;
					updateLineBreakState(c);
				} else if (inWhitespace) {
					inWhitespace = false;
					writeLineBreakOrSpace();
					buf[pos++] = c;
				} else {
					buf[pos++] = c;
				}
			}
		}

		/*
		 * \r\n => CRLF \r[^\n] => CR \r$ => CR [^\r]\n => LF ^\n => LF
		 */
		private void updateLineBreakState(char c) {
			switch (lineBreakState) {
			case INIT:
				if (c == '\r') {
					lineBreakState = SAW_CR;
				} else if (c == '\n') {
					lineBreakState = LINEBREAK_LF;
				}
				break;
			case SAW_CR:
				if (c == '\n') {
					lineBreakState = LINEBREAK_CRLF;
				} else {
					lineBreakState = LINEBREAK_CR;
				}
			}
		}

		private void writeLineBreakOrSpace() {
			switch (lineBreakState) {
			case SAW_CR:
				// whitespace ended with CR, fall through
			case LINEBREAK_CR:
				buf[pos++] = '\r';
				break;
			case LINEBREAK_CRLF:
				buf[pos++] = '\r';
				// fall through
			case LINEBREAK_LF:
				buf[pos++] = '\n';
				break;
			case AT_BEGINNING:
				// ignore leading whitespace
				break;
			case INIT:
			case SINGLE_LINE:
				buf[pos++] = ' ';
			}
			lineBreakState = (singleLine) ? SINGLE_LINE : INIT;
		}

		private void flushInternal() throws IOException {
			out.write(buf, 0, pos);
			pos = 0;
		}

		public void flush() throws IOException {
			flushInternal();
			out.flush();
		}

		public void close() throws IOException {
			flushInternal();
		}
	}
}
